package ms.jasim.framework;

import java.util.ArrayList;
import java.util.List;

import ms.jacrim.framework.MessageOutput;
import ms.jacrim.pddl.PddlSolution;
import ms.jasim.framework.SolutionSimulator.SimulatorArg;
import ms.spm.IAppContext;
import ms.utils.Event;
import ms.utils.EventListImpl;
import ms.utils.ExceptionUtil;
import ms.utils.NamedList;

public class EventSimulation implements Runnable {

	private List<JEventInstance> events;
	private IAppContext context;
	private MessageOutput stdout;
	private List<Solution> solutions;
	private List<PddlSolution> initalSolutions;
	private EventListImpl<SimulationProgressArg> progressListener;
	private PddlModel model;
	private int totalSolutions = 0;
	private SimulationProgressArg progressArg;
	private Thread simulThread;
	private SolutionGenerator generator;

	public class Solution {
		private PddlModel model;
		private PddlSolution solution;
		private int time;
		private ArrayList<Solution> branches;
		// private float[] evaluatorValue;
		private Solution parent;
		private String name;
		protected int maxSolution;
		protected int maxProcessTime;
		private ArrayList<JEventInstance> events;
		private NamedList<EvaluatorResult> evaluateResult;
		private int endTime;
		private SolutionSimulator simulator;
		
		public Solution(Solution parent, PddlModel model, PddlSolution solution, int time, List<JEventInstance> events) {
			this.model = model.clone();
			this.time = endTime = time;
			this.solution = solution;
			this.parent = parent;
			this.events = new ArrayList<JEventInstance>(events);
			name = solution.getName();
			evaluateResult = model.evaluateSolution(context, solution);
		}

		protected NamedList<PddlSolution> getBranches(int maxBranches) {
			try {
				generator = new SolutionGenerator(context);
				generator.startPlanner(model, maxBranches, false);
				if (generator.getSolutions() != null)
					totalSolutions += generator.getSolutions().size();
				NamedList<PddlSolution> result = generator.getSolutions();
				generator = null;
				return result;
			} catch (Exception e) {
				ExceptionUtil.handle(e, context);
				return null;
			}
		}

		public void run() throws Exception {
			stdout.write("Event", "Simulating solution: %s.\n", name);
			fireProgressListener(this, SimulationEvent.VISITING_NODE, (String) null);
			
			simulator = new SolutionSimulator(context, model, solution);
			simulator.addListener(new Event<SimulatorArg>() {
				@Override
				public void run(Object sender, SimulatorArg arg) {
					ArrayList<JEventInstance> triggerEvent = null;
					StringBuilder eventNames = null;
					boolean replan = false;
					
					// browse over event instances to see which one is
					// triggered?
					for (int i = events.size() - 1; i >= 0; i--) {
						JEventInstance event = events.get(i);
						if (event.canTriggerNow(arg)) {
							stdout.write("Event", "Event %s is triggered at time %d in solution %s.\n", event.getID(), arg.getTime(), name);
							if (triggerEvent == null)
								triggerEvent = new ArrayList<JEventInstance>();
							triggerEvent.add(event);
						}
					}
					
					if (triggerEvent != null) {
						eventNames = new StringBuilder();

						for (JEventInstance event : triggerEvent) {
							// record events happen
							JEventType.EventReplanOption opt = event.getParamValue(JEventType.REPLAN_OPT);
							if (eventNames.length() > 0)
								eventNames.append(',');
							eventNames.append(event.getID());
							if (event.Type.react(context, model, event, arg.getSimulator())) {
								replan = replan | opt.isReplan();
								if (opt.isReplan()) {
									maxSolution = Math.max(maxSolution, opt.getNumberOfSolutions());
									maxProcessTime = Math.max(maxProcessTime, opt.getMaxProcessTime());
									eventNames.append("!");
								}
							}
							if (opt.isOnceTimeEvent())
								events.remove(event);
						}
						
						if (replan) {
							// reflect current situation to the model
							for (PddlSolution.Action act : arg.getSimulator().getFinishedActions())
								model.reflectFinishedAction(act);
							arg.setCancel(true);
						}
						if (triggerEvent.size() > 0) {
							fireProgressListener(Solution.this, SimulationEvent.EVENT_TRIGGERED, arg.getTime() + ": " + eventNames.toString());
						}
					}
				}
			});
			
			endTime = simulator.simulate(time, 9999);
			
			if (maxSolution > 0 && !simulator.isCompleted()) {
				NamedList<PddlSolution> sols = getBranches(maxSolution);
				if (sols != null) {
					branches = new ArrayList<Solution>();
					for (int j = 0; j < sols.size() && sols.get(j).ActionCount() > 0; j++) {
						Solution child = new Solution(this, model, sols.get(j), endTime - 1, events);
						child.setName(name + "-->" + j);
						sols.get(j).setName(name + "-->" + j);
						branches.add(child);
					}
				}
			}
			fireProgressListener(this, SimulationEvent.VISITED_NODE, null);
			if (branches != null) {
				NamedList<EvaluatorResult> evRes = model.evaluateSolution(context, simulator.getFinishedActions());
				for (Solution sol : branches) {
					// combine old cost
					for (PddlModel.Evaluator eval : model.Evaluators) {
						int idx = sol.evaluateResult.indexOf(eval.getName());
						EvaluatorResult combine = eval.getInstance().combine(evRes.get(eval.getName()), sol.evaluateResult.get(idx));
						sol.evaluateResult.set(idx, combine);
					}
					sol.run();
				}
			}
			else if (simulator.isCompleted())
				stdout.write("Event", "%s is successfully simulated at time %d. \n\n", name, endTime);
			else
				stdout.write("Event", "%s is stucked at time %d. \n\n", name, endTime);

		}

		public Solution getParent() {
			return parent;
		}

		public String getName() {
			return name;
		}

		private void setName(String value) {
			name = value;
		}

		public List<EvaluatorResult> getEvaluateResult() {
			return evaluateResult;
		}

		public PddlSolution getPddlSolution() {
			return solution;
		}

		@Override
		public String toString() {
			return name;
		}

		public PddlModel getModel() {
			return model;
		}

		public float getCost() {
			if (evaluateResult != null && evaluateResult.size() > 0)
				return evaluateResult.get(0).getValue();
			return 0f;
		}
		
		public EvaluatorResult getEvaluatorMetric(String evalName) {
			return evaluateResult.get(evalName);
		}
		
		public int getTime() {
			return time;
		}
		
		public boolean isEnd() {
			return branches == null || branches.size() == 0;
		}

		public Number getEndTime() {
			return endTime;
		}
	
		public SolutionSimulator getSimulator() {
			return simulator;
		}
	}

	public enum SimulationEvent {
		NA, VISITING_NODE, EVENT_TRIGGERED, VISITED_NODE
	}

	public class SimulationProgressArg {
		private int progress;
		private int total;
		private Solution solution;
		private String message;
		private SimulationEvent event;

		public SimulationProgressArg() {

		}

		public SimulationProgressArg(Solution solution, int progress, int total) {
			this.progress = progress;
			this.total = total;
			this.solution = solution;
		}

		public int getProgress() {
			return progress;
		}

		public int getTotal() {
			return total;
		}

		public Solution getSolution() {
			return solution;
		}

		protected void setProgress(int progress) {
			this.progress = progress;
		}

		protected void setTotal(int total) {
			this.total = total;
		}

		protected void setSolution(Solution solution) {
			this.solution = solution;
		}

		public boolean isCompleted() {
			return total == progress;
		}

		public void setMessage(String message) {
			this.message = message;
		}

		public String getMessage() {
			return message;
		}

		protected void setEvent(SimulationEvent event) {
			this.event = event;
		}

		public SimulationEvent getEvent() {
			return event;
		}
	}

	public EventSimulation(IAppContext context, PddlModel model, List<JEventInstance> events, List<PddlSolution> solutions) {
		this.context = context;
		stdout = context.getService(MessageOutput.class);
		this.events = events;
		this.model = model;
		initalSolutions = solutions;
		progressArg = new SimulationProgressArg(null, 0, totalSolutions = calculateTotalSolution());
	}

	private int calculateTotalSolution() {
		// int idx = 0;
		int total = initalSolutions.size();
		// int lastTotal = total;
		// do {
		// for (; idx < events.size() && !(events.get(idx).Type instanceof
		// ReplanEvent); idx++)
		// ;
		// if (idx < events.size()) {
		// int nb = events.get(idx).getParamValue(ReplanEvent.MAX_SOLUTION);
		// lastTotal = (int) Math.pow(lastTotal, nb);
		// total += lastTotal;
		// }
		// } while (idx < events.size());
		return total;
	}

	@Override
	public void run() {
		solutions = new ArrayList<Solution>();
		fireProgressStarted(initalSolutions.size());
		for (PddlSolution isol : initalSolutions) {
			try {
				Solution sol = new Solution(null, model, isol, 0, events);
				solutions.add(sol);
				sol.run();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		fireProgressCompleted();
		simulThread = null;
	}

	public void start() {
		simulThread = new Thread(this);
		simulThread.start();
	}

	@SuppressWarnings("deprecation")
	public void stop() {
		if (generator != null) {
			generator.stopPlanner();
			generator = null;
		}
		if (simulThread != null) {
			simulThread.stop();
			simulThread = null;
			fireProgressCompleted();
		}

	}

	public int getMileStone(int startEventIdx, int time) {
		for (int i = startEventIdx; i < events.size(); i++) {
			if (events.get(i).Type instanceof ReplanEvent)
				return events.get(i).getTime();
		}
		return Integer.MAX_VALUE;
	}

	protected void fireProgressListener(Solution sol, SimulationEvent status, String message) {
		if (message == null)
			message = "Working on " + sol.getName();
		fireProgressListener(totalSolutions, progressArg.progress + (status == SimulationEvent.VISITED_NODE ? 1 : 0), sol, message, status);
	}

	protected void fireProgressStarted(int total) {
		fireProgressListener(totalSolutions = total, 0, null, "", SimulationEvent.NA);
	}

	protected void fireProgressCompleted() {
		fireProgressListener(100, 100, null, "Completed", SimulationEvent.NA);
	}

	protected void fireProgressListener(int total, int progress, Solution sol, String message, SimulationEvent event) {
		progressArg.total = total;
		progressArg.progress = progress;
		progressArg.solution = sol;
		progressArg.message = message;
		progressArg.event = event;
		progressListener.invoke(this, progressArg);
	}

	public void addProgressListener(Event<SimulationProgressArg> e) {
		if (progressListener == null)
			progressListener = new EventListImpl<SimulationProgressArg>();
		
		if (progressListener.indexOf(e) < 0)
			progressListener.add(e);
	}

	public void removeProgressListener(Event<SimulationProgressArg> e) {
		if (progressListener != null)
			progressListener.remove(e);
	}
}
